/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.internal;

import java.beans.*;
import java.util.*;
import java.util.Map.Entry;
import ch.simpleel.*;
import ch.simpleel.accessors.*;
import ch.trackedbean.common.*;
import ch.trackedbean.loaders.lazy.*;
import ch.trackedbean.tracking.*;

/**
 * Class holding the status of a bean and it's properties.
 * 
 * @param <T> The owner type
 * @author M. Hautle
 */
public class BeanStatusImpl<T> implements IChildStatus, IBeanStatus<T> {
    /** Object representing null */
    private static final Object NULL = new Object();

    /** Default status (used if no status is set) */
    private static final Status DEFAULT_STATUS = new Status(BeanStatusFlag.UNCHANGED, null);

    /** The owner object of this Status object */
    private final T owner;

    /** A map holding the status of all properties */
    private final Map<String, Status> status = new HashMap<String, Status>();

    /**
     * A map holding the initial values of all properties.<br>
     * The initial value of a property is set when the first time {@link #propertyChanged(String, Object, Object)} is called. {@link #NULL} replaces null so
     * that there is a difference between null and not yet set.
     */
    private final Map<String, Object> initialValues = new HashMap<String, Object>();

    /** The property change support for the property status values. */
    private final PropertyChangeSupport statusChange;

    /** Current/last known call id used in a thread. This is used to identify cyclic calls. */
    private final ThreadLocal<Object> currentCallId = new ThreadLocal<Object>();

    /** A list with structure parents or null */
    private List<OwnerEntry> structureParents = null;

    /** The number of properties with the {@link BeanStatusFlag#ERROR} flag */
    private int errorCount = 0;

    /** The number of properties with the {@link BeanStatusFlag#CHANGED} flag */
    private int changedCount = 0;

    /**
     * The number of children containing errors in their structure ({@link BeanStatusFlag#ERROR} flag)<br>
     * The status property of this bean is handled like the status of a child.
     */
    private volatile int structureErrorCount = 0;

    /**
     * The number of children containing changes in their structure ({@link BeanStatusFlag#CHANGED} flag)<br>
     * The status property of this bean is handled like the status of a child.
     */
    private volatile int structureChangedCount = 0;

    /**
     * Default constructor
     * 
     * @param owner The owner Bean of this Status object
     */
    public BeanStatusImpl(final T owner) {
        this.owner = owner;
        statusChange = new PropertyChangeSupport(owner);
        status.put(BEAN_STATUS, new Status(BeanStatusFlag.UNCHANGED));
        status.put(BEAN_STRUCTURE_STATUS, new Status(BeanStatusFlag.UNCHANGED));
    }

    /**
     * {@inheritDoc}
     */
    public synchronized Status getStatus(final String property) {
        final Status s = status.get(property);
        return s == null ? DEFAULT_STATUS : s;
    }

    /**
     * Checks if the given field is an uninitialized (lazy) field.<br>
     * <b>The lazy status will be reseted by this call!</b><br>
     * <br>
     * <b>This method is only for internal use!</b>
     * 
     * @param property The name of the property
     * @return True if the property has to be loaded
     */
    public synchronized boolean loadField(final String property) {
        final Status s = status.get(property);
        if (s == null || s.getFlag() != BeanStatusFlag.UNDEFINED)
            return false;
        status.remove(property);
        fireStatusChange(property, s, DEFAULT_STATUS);
        return true;
    }

    /**
     * Sets the field state of the given property to {@link BeanStatusFlag#UNDEFINED} so that lazy loading will be activated on this field (if the field was
     * annotated with {@link LoadLazy}).<br>
     * 
     * @param property The name of the property
     */
    public void setLazy(final String property) {
        setStatus(property, BeanStatusFlag.UNDEFINED);
    }

    /**
     * Registers the given child as structure child and unregisters the old one.
     * 
     * @param oldValue The previously registered child or null
     * @param newValue The new child to register or null
     * @param property The property in which the child is hold
     */
    public void registerChild(final Object oldValue, final Object newValue, final String property) {
        if (oldValue instanceof StatusInformationInternal)
            ((StatusInformationInternal) oldValue)._getBeanStatus().unregisterOwner(this);
        if (newValue instanceof StatusInformationInternal)
            ((StatusInformationInternal) newValue)._getBeanStatus().registerOwner(this, property);
    }

    /**
     * Creates a {@link StatusList} for the new list and register it as structure child. The old list will be unregistered.
     * 
     * @param old The previously registered list or null
     * @param child The new list to register or null
     * @param property The property in which the list is hold
     * @return The new list as {@link StatusList} or null if null was passed
     */
    @SuppressWarnings("unchecked")
    public List registerChildList(final List old, final List child, final String property) {
        if (old != null)
            StatusList.removed(old, this);
        return child == null ? null : StatusList.create(child, this, property);
    }

    /**
     * Adds the given owner to the {@link #structureParents} list and calls {@link #childStructureStatusChanged(Object,int, int)} on the added owner with the
     * current state and {@link BeanStatusFlag#UNCHANGED} as old state
     * 
     * @param owner The owner to add
     * @param property The property in which the owner holds this bean or null if not in a property
     */
    public synchronized void registerOwner(IChildStatus owner, String property) {
        if (structureParents == null)
            structureParents = new ArrayList<OwnerEntry>(1);
        structureParents.add(new OwnerEntry(owner, property));
        // add our structure counts
        updateOwnerCounts(owner, structureChangedCount, structureErrorCount);
    }

    /**
     * Removes the given owner form the {@link #structureParents} list and calls {@link #childStructureStatusChanged(Object,int, int)} on the remove owner
     * setting the new state to {@link BeanStatusFlag#UNCHANGED}
     * 
     * @param owner The owner to remove
     */
    public synchronized void unregisterOwner(final IChildStatus owner) {
        if (structureParents == null || !removeOwner(owner))
            throw new IllegalStateException("Tried to remove " + owner + " from the structure parents list, but it was already removed!");
        // subtract our structure counts
        updateOwnerCounts(owner, -structureChangedCount, -structureErrorCount);
        if (structureParents.isEmpty())
            structureParents = null;
    }

    /**
     * Updates the structure counts of our owner.
     * 
     * @param owner The owner - the one to inform
     * @param changeCount The change count to propagate
     * @param errorCount The error count to propagate
     */
    private void updateOwnerCounts(IChildStatus owner, int changeCount, int errorCount) {
        // will this call create a cyclic data structure?
        if (owner.isChildOf(new Object(), this)) {
            // avoid that we process the event - our state is already up to date
            final Object id = new Object();
            currentCallId.set(id);
            // CHECKME stimmt das so?
            // cause our owner is a child of use we subtract his counts from ours - we don't want count them twice
            int change = changeCount > 0 ? changeCount - owner.getStructureChangeCount() : changeCount + owner.getStructureChangeCount();
            int error = errorCount > 0 ? errorCount - owner.getStructureErrorCount() : errorCount + owner.getStructureErrorCount();
            owner.childStructureStatusChanged(id, change, error);
        } else {
            owner.childStructureStatusChanged(new Object(), changeCount, errorCount);
        }
    }

    /**
     * Removes the given owner from the owner list.
     * 
     * @param owner The owner
     * @return True if it was removed
     */
    private boolean removeOwner(IChildStatus owner) {
        for (int i = 0, cnt = structureParents.size(); i < cnt; i++) {
            if (owner == structureParents.get(i).owner) {
                structureParents.remove(i);
                return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void childContentChanged(String property, Object child) {
        setStatus(property, BeanStatusFlag.CHANGED);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BeanStatusFlag getStatusFlag() {
        return getStatus().getFlag();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BeanStatusFlag getStructureStatusFlag() {
        return getStructureStatus().getFlag();
    }

    /**
     * Called by the owning bean if a property has changed
     * 
     * @param property The property name
     * @param oldValue The old value
     * @param newValue The new value
     */
    public synchronized void propertyChanged(final String property, final Object oldValue, final Object newValue) {
        // ignore same values
        if (oldValue == newValue || oldValue != null && oldValue.equals(newValue)) {
            resetTechnicalError(property, newValue);
            return;
        }
        Object init = initialValues.get(property);
        // first property change -> set initial property
        if (init == null) {
            initialValues.put(property, oldValue == null ? NULL : oldValue);
            setStatus(property, BeanStatusFlag.CHANGED);
            contentChanged();
            return;
        }
        // replace null placeholder
        if (init == NULL)
            init = null;
        setStatus(property, init == newValue || init != null && init.equals(newValue) ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED);
        contentChanged();
    }

    /**
     * Resets an {@link BeanStatusFlag#TECHNICAL_ERROR} on the given property and sets it on {@link BeanStatusFlag#CHANGED} or {@link BeanStatusFlag#UNCHANGED}
     * depending on the property value.
     * 
     * @param property The property
     * @param newValue The new value
     */
    private void resetTechnicalError(final String property, final Object newValue) {
        final Status s = status.get(property);
        if (s == null || s.getFlag() != BeanStatusFlag.TECHNICAL_ERROR)
            return;
        Object init = initialValues.get(property);
        // if init wasn't set then the property wasn't yet touched (before the error), so now it's unchanged again
        if (init == null) {
            setStatus(property, BeanStatusFlag.UNCHANGED);
            return;
        }
        // replace null placeholder
        if (init == NULL)
            init = null;
        setStatus(property, init == newValue || init != null && init.equals(newValue) ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED);
    }

    /**
     * Inform the parents about a content change.
     */
    private void contentChanged() {
        if (structureParents == null)
            return;
        // inform the owners
        for (OwnerEntry o : structureParents)
            o.fireChange(this);
    }

    /**
     * {@inheritDoc}
     */
    public void setStatus(final String property, final BeanStatusFlag flag) {
        setStatus(property, flag, null);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void setStatus(final String property, final BeanStatusFlag flag, final String message) {
        // don't touch the bean status
        if (BEAN_STATUS.equals(property) || BEAN_STRUCTURE_STATUS.equals(property))
            return;
        final Status old = getStatus(property);
        // if the old status corresponds to the new one, then do nothing
        if (old.equals(flag, message))
            return;
        final Status newStatus = new Status(flag, message);
        status.put(property, newStatus);
        // compute the new error and changed counts
        errorCount += flag.errorValue - old.getFlag().errorValue;
        changedCount += flag.changedValue - old.getFlag().changedValue;
        // update the beans status
        final BeanStatusFlag oldStatus = getStatusFlag();
        if (errorCount == 0) {
            final BeanStatusFlag newFlag = changedCount == 0 ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED;
            if (newFlag != oldStatus)
                setBeanStatus(new Status(newFlag));
        } else if (BeanStatusFlag.ERROR != oldStatus) {
            setBeanStatus(new Status(BeanStatusFlag.ERROR));
        }
        fireStatusChange(property, old, newStatus);
    }

    /**
     * Sets the bean status and calls {@link #childStructureStatusChanged(Object,int, int)} with the old and the new bean status (the bean status is handled
     * like a child of the bean structure)
     * 
     * @param newStatus The new bean status
     */
    private void setBeanStatus(final Status newStatus) {
        final Status old = status.put(BEAN_STATUS, newStatus);
        // inform the structure status about the state change
        final BeanStatusFlag oldFlag = old.getFlag();
        final BeanStatusFlag newFlag = newStatus.getFlag();
        childStructureStatusChanged0(new Object(), newFlag.changedValue - oldFlag.changedValue, newFlag.errorValue - oldFlag.errorValue);
        fireStatusChange(BEAN_STATUS, old, newStatus);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized boolean isChildOf(Object requestId, IChildStatus element) {
        // if there are no parents we can't be a child of the passed element :)
        if (structureParents == null)
            return false;
        // ignore already processed request
        if (requestId == currentCallId.get())
            return false;
        currentCallId.set(requestId);
        for (OwnerEntry p : structureParents) {
            // direct child or maybe a grand child?
            if (p.owner == element || p.owner.isChildOf(requestId, element))
                return true;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void childStructureStatusChanged(Object requestId, int changeDelta, int errorDelta) {
        // ignore changes already processed (may occur if there is a cycle in the data structure)
        if (requestId != currentCallId.get())
            childStructureStatusChanged0(requestId, changeDelta, errorDelta);
    }

    /**
     * Internal {@link #childStructureStatusChanged(Object, int, int)} method.<br>
     * Does not check if we are the source of the change.
     * 
     * @param requestId The id of this call (used to identify if a call was already processes in a cyclic structure)
     * @param changeDelta The difference in the number of structure changes (of a whole subtree)
     * @param errorDelta The difference in the number of structure errors (of a whole subtree)
     */
    private void childStructureStatusChanged0(Object requestId, final int changeDelta, final int errorDelta) {
        // set the message id
        currentCallId.set(requestId);
        //
        structureErrorCount += errorDelta;
        structureChangedCount += changeDelta;
        // CHECKME wie kommt det??
        if (structureErrorCount < 0) {
            throw new IllegalStateException("Negative error count on " + owner.toString());
        }
        // // CHECKME wie kommt det??
        if (structureChangedCount < 0) {
            throw new IllegalStateException("Negative change count on " + owner.toString());
        }

        final BeanStatusFlag newFlag;
        if (structureErrorCount == 0)
            newFlag = structureChangedCount == 0 ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED;
        else
            newFlag = BeanStatusFlag.ERROR;
        // inform the owners status about the state change
        if (structureParents != null)
            for (final OwnerEntry o : structureParents)
                o.owner.childStructureStatusChanged(requestId, changeDelta, errorDelta);
        // and set the new structure state
        if (newFlag != getStructureStatusFlag())
            setBeanStructureStatus(new Status(newFlag));
    }

    /**
     * Sets the bean structure status and informs the owners about the change
     * 
     * @param newStatus The new status to set
     */
    private void setBeanStructureStatus(final Status newStatus) {
        final Status old = status.put(BEAN_STRUCTURE_STATUS, newStatus);
        fireStatusChange(BEAN_STRUCTURE_STATUS, old, newStatus);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void forceBeanStatus(final BeanStatusFlag flag, final String message) {
        for (Iterator<Entry<String, Status>> i = status.entrySet().iterator(); i.hasNext();) {
            final Entry<String, Status> e = i.next();
            final String prop = e.getKey();
            // skip bean states
            if (BEAN_STATUS.equals(prop) || BEAN_STRUCTURE_STATUS.equals(prop))
                continue;
            i.remove();
            fireStatusChange(prop, e.getValue(), DEFAULT_STATUS);
        }
        errorCount = 0;
        changedCount = 0;
        setBeanStatus(new Status(flag, message));
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public synchronized void resetToInitialValues() throws ResetException {
        try {
            for (IValueAccessor p : ValueAccessorManager.getPropertyAccessors(owner.getClass())) {
                final Object v = initialValues.get(p.getPath());
                if (v != null)
                    p.setValue(owner, v != NULL ? v : null);
            }
        } catch (Exception e) {
            throw new ResetException("Error while resetting the values", e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void clearInitialValues() {
        clearInitialValues(false);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void clearInitialValues(boolean resetState) {
        initialValues.clear();
        if (resetState)
            forceBeanStatus(BeanStatusFlag.UNCHANGED, null);
    }

    /**
     * {@inheritDoc}
     */
    public Status getStatus() {
        return getStatus(BEAN_STATUS);
    }

    /**
     * {@inheritDoc}
     */
    public Status getStructureStatus() {
        return getStatus(BEAN_STRUCTURE_STATUS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getStructureChangeCount() {
        return structureChangedCount;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getStructureErrorCount() {
        return structureErrorCount;
    }

    /**
     * {@inheritDoc}
     */
    public T getOwner() {
        return owner;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addStatusChangeListener(PropertyChangeListener l) {
        statusChange.addPropertyChangeListener(l);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeStatusChangeListener(PropertyChangeListener l) {
        statusChange.removePropertyChangeListener(l);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addStatusChangeListener(String property, PropertyChangeListener l) {
        statusChange.addPropertyChangeListener(property, l);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeStatusChangeListener(String property, PropertyChangeListener l) {
        statusChange.removePropertyChangeListener(property, l);
    }

    /**
     * Fires a property changed event for an {@link Status} change.
     * 
     * @param property The property name
     * @param oldStatus The old status
     * @param newStatus The new status
     */
    private void fireStatusChange(String property, Status oldStatus, Status newStatus) {
        try {
            statusChange.firePropertyChange(property, oldStatus, newStatus);
        } catch (Exception e) {
            DefaultExceptionHandler.getDefault().exceptionOccurred(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return owner + " (" + getStatusFlag() + ", " + getStructureStatusFlag() + ")";
    }
}
